home *** CD-ROM | disk | FTP | other *** search
/ Total Network Tools 2002 / NextStepPublishing-TotalNetworkTools2002-Win95.iso / Archive / Misc Servers / Zope.exe / FTP_SERVER.PY < prev    next >
Encoding:
Python Source  |  2000-07-05  |  32.5 KB  |  1,130 lines

  1. # -*- Mode: Python; tab-width: 4 -*-
  2.  
  3. #    Author: Sam Rushing <rushing@nightmare.com>
  4. #    Copyright 1996-2000 by Sam Rushing
  5. #                         All Rights Reserved.
  6. #
  7.  
  8. RCS_ID =  '$Id: ftp_server.py,v 1.12.4.1 2000/07/05 14:21:44 brian Exp $'
  9.  
  10. # An extensible, configurable, asynchronous FTP server.
  11. # All socket I/O is non-blocking, however file I/O is currently
  12. # blocking.  Eventually file I/O may be made non-blocking, too, if it
  13. # seems necessary.  Currently the only CPU-intensive operation is
  14. # getting and formatting a directory listing.  [this could be moved
  15. # into another process/directory server, or another thread?]
  16. #
  17. # Only a subset of RFC 959 is implemented, but much of that RFC is
  18. # vestigial anyway.  I've attempted to include the most commonly-used
  19. # commands, using the feature set of wu-ftpd as a guide.
  20.  
  21. import asyncore
  22. import asynchat
  23.  
  24. import os
  25. import regsub
  26. import socket
  27. import stat
  28. import string
  29. import sys
  30. import time
  31.  
  32. # TODO: implement a directory listing cache.  On very-high-load
  33. # servers this could save a lot of disk abuse, and possibly the
  34. # work of computing emulated unix ls output.
  35.  
  36. # Potential security problem with the FTP protocol?  I don't think
  37. # there's any verification of the origin of a data connection.  Not
  38. # really a problem for the server (since it doesn't send the port
  39. # command, except when in PASV mode) But I think a data connection
  40. # could be spoofed by a program with access to a sniffer - it could
  41. # watch for a PORT command to go over a command channel, and then
  42. # connect to that port before the server does.
  43.  
  44. # Unix user id's:
  45. # In order to support assuming the id of a particular user,
  46. # it seems there are two options:
  47. # 1) fork, and seteuid in the child
  48. # 2) carefully control the effective uid around filesystem accessing
  49. #    methods, using try/finally. [this seems to work]
  50.  
  51. VERSION = string.split(RCS_ID)[2]
  52.  
  53. from counter import counter
  54. import producers
  55. import status_handler
  56. import logger
  57. import string
  58.  
  59. class ftp_channel (asynchat.async_chat):
  60.  
  61.     # defaults for a reliable __repr__
  62.     addr = ('unknown','0')
  63.  
  64.     # unset this in a derived class in order
  65.     # to enable the commands in 'self.write_commands'
  66.     read_only = 1
  67.     write_commands = ['appe','dele','mkd','rmd','rnfr','rnto','stor','stou']
  68.  
  69.     restart_position = 0
  70.  
  71.     # comply with (possibly troublesome) RFC959 requirements
  72.     # This is necessary to correctly run an active data connection
  73.     # through a firewall that triggers on the source port (expected
  74.     # to be 'L-1', or 20 in the normal case).
  75.     bind_local_minus_one = 0
  76.  
  77.     def __init__ (self, server, conn, addr):
  78.         self.server = server
  79.         self.current_mode = 'a'
  80.         self.addr = addr
  81.         asynchat.async_chat.__init__ (self, conn)
  82.         self.set_terminator ('\r\n')
  83.  
  84.         # client data port.  Defaults to 'the same as the control connection'.
  85.         self.client_addr = (addr[0], 21)
  86.  
  87.         self.client_dc = None
  88.         self.in_buffer = ''
  89.         self.closing = 0
  90.         self.passive_acceptor = None
  91.         self.passive_connection = None
  92.         self.filesystem = None
  93.         self.authorized = 0
  94.         # send the greeting
  95.         self.respond (
  96.             '220 %s FTP server (Medusa Async V%s [experimental]) ready.' % (
  97.                 self.server.hostname,
  98.                 VERSION
  99.                 )
  100.             )
  101.  
  102. #    def __del__ (self):
  103. #        print 'ftp_channel.__del__()'
  104.  
  105.     # --------------------------------------------------
  106.     # async-library methods
  107.     # --------------------------------------------------
  108.  
  109.     def handle_expt (self):
  110.         # this is handled below.  not sure what I could
  111.         # do here to make that code less kludgish.
  112.         pass
  113.  
  114.     def collect_incoming_data (self, data):
  115.         self.in_buffer = self.in_buffer + data
  116.         if len(self.in_buffer) > 4096:
  117.             # silently truncate really long lines
  118.             # (possible denial-of-service attack)
  119.             self.in_buffer = ''
  120.  
  121.     def found_terminator (self):
  122.  
  123.         line = self.in_buffer
  124.  
  125.         if not len(line):
  126.             return
  127.  
  128.         sp = string.find (line, ' ')
  129.         if sp != -1:
  130.             line = [line[:sp], line[sp+1:]]
  131.         else:
  132.             line = [line]
  133.  
  134.         command = string.lower (line[0])
  135.         # watch especially for 'urgent' abort commands.
  136.         if string.find (command, 'abor') != -1:
  137.             # strip off telnet sync chars and the like...
  138.             while command and command[0] not in string.letters:
  139.                 command = command[1:]
  140.         fun_name = 'cmd_%s' % command
  141.         if command != 'pass':
  142.             self.log ('<== %s' % repr(self.in_buffer)[1:-1])
  143.         else:
  144.             self.log ('<== %s' % line[0]+' <password>')
  145.         self.in_buffer = ''
  146.         if not hasattr (self, fun_name):
  147.             self.command_not_understood (line[0])
  148.             return
  149.         fun = getattr (self, fun_name)
  150.         if (not self.authorized) and (command not in ('user', 'pass', 'help', 'quit')):
  151.             self.respond ('530 Please log in with USER and PASS')
  152.         elif (not self.check_command_authorization (command)):
  153.             self.command_not_authorized (command)
  154.         else:
  155.             try:
  156.                 result = apply (fun, (line,))
  157.             except:
  158.                 self.server.total_exceptions.increment()
  159.                 (file, fun, line), t,v, tbinfo = asyncore.compact_traceback()
  160.                 if self.client_dc:
  161.                     try:
  162.                         self.client_dc.close()
  163.                     except:
  164.                         pass
  165.                 self.respond (
  166.                     '451 Server Error: %s, %s: file: %s line: %s' % (
  167.                         t,v,file,line,
  168.                         )
  169.                     )
  170.  
  171.     closed = 0
  172.     def close (self):
  173.         if not self.closed:
  174.             self.closed = 1
  175.             if self.passive_acceptor:
  176.                 self.passive_acceptor.close()
  177.             if self.client_dc:
  178.                 self.client_dc.close()
  179.             self.server.closed_sessions.increment()
  180.             asynchat.async_chat.close (self)
  181.  
  182.     # --------------------------------------------------
  183.     # filesystem interface functions.
  184.     # override these to provide access control or perform
  185.     # other functions.
  186.     # --------------------------------------------------
  187.  
  188.     def cwd (self, line):
  189.         return self.filesystem.cwd (line[1])
  190.  
  191.     def cdup (self, line):
  192.         return self.filesystem.cdup()
  193.  
  194.     def open (self, path, mode):
  195.         return self.filesystem.open (path, mode)
  196.  
  197.     # returns a producer
  198.     def listdir (self, path, long=0):
  199.         return self.filesystem.listdir (path, long)
  200.  
  201.     def get_dir_list (self, line, long=0):
  202.         # we need to scan the command line for arguments to '/bin/ls'...
  203.         args = line[1:]
  204.         path_args = []
  205.         for arg in args:
  206.             if arg[0] != '-':
  207.                 path_args.append (arg)
  208.             else:
  209.                 # ignore arguments
  210.                 pass
  211.         if len(path_args) < 1:
  212.             dir = '.'
  213.         else:
  214.             dir = path_args[0]
  215.         return self.listdir (dir, long)
  216.  
  217.     # --------------------------------------------------
  218.     # authorization methods
  219.     # --------------------------------------------------
  220.  
  221.     def check_command_authorization (self, command):
  222.         if command in self.write_commands and self.read_only:
  223.             return 0
  224.         else:
  225.             return 1
  226.  
  227.     # --------------------------------------------------
  228.     # utility methods
  229.     # --------------------------------------------------
  230.  
  231.     def log (self, message):
  232.         self.server.logger.log (
  233.             self.addr[0],
  234.             '%d %s' % (
  235.                 self.addr[1], message
  236.                 )
  237.             )
  238.  
  239.     def respond (self, resp):
  240.         self.log ('==> %s' % resp)
  241.         self.push (resp + '\r\n')
  242.  
  243.     def command_not_understood (self, command):
  244.         self.respond ("500 '%s': command not understood." % command)
  245.  
  246.     def command_not_authorized (self, command):
  247.         self.respond (
  248.             "530 You are not authorized to perform the '%s' command" % (
  249.                 command
  250.                 )
  251.             )
  252.  
  253.     def make_xmit_channel (self):
  254.         # In PASV mode, the connection may or may _not_ have been made
  255.         # yet.  [although in most cases it is... FTP Explorer being
  256.         # the only exception I've yet seen].  This gets somewhat confusing
  257.         # because things may happen in any order...
  258.         pa = self.passive_acceptor
  259.         if pa:
  260.             if pa.ready:
  261.                 # a connection has already been made.
  262.                 conn, addr = self.passive_acceptor.ready
  263.                 cdc = xmit_channel (self, addr)
  264.                 cdc.set_socket (conn)
  265.                 cdc.connected = 1
  266.                 self.passive_acceptor.close()
  267.                 self.passive_acceptor = None                
  268.             else:
  269.                 # we're still waiting for a connect to the PASV port.
  270.                 cdc = xmit_channel (self)
  271.         else:
  272.             # not in PASV mode.
  273.             ip, port = self.client_addr
  274.             cdc = xmit_channel (self, self.client_addr)
  275.             cdc.create_socket (socket.AF_INET, socket.SOCK_STREAM)
  276.             if self.bind_local_minus_one:
  277.                 cdc.bind (('', self.server.port - 1))
  278.             try:
  279.                 cdc.connect ((ip, port))
  280.             except socket.error, why:
  281.                 self.respond ("425 Can't build data connection")
  282.         self.client_dc = cdc
  283.  
  284.     # pretty much the same as xmit, but only right on the verge of
  285.     # being worth a merge.
  286.     def make_recv_channel (self, fd):
  287.         pa = self.passive_acceptor
  288.         if pa:
  289.             if pa.ready:
  290.                 # a connection has already been made.
  291.                 conn, addr = pa.ready
  292.                 cdc = recv_channel (self, addr, fd)
  293.                 cdc.set_socket (conn)
  294.                 cdc.connected = 1
  295.                 self.passive_acceptor.close()
  296.                 self.passive_acceptor = None                
  297.             else:
  298.                 # we're still waiting for a connect to the PASV port.
  299.                 cdc = recv_channel (self, None, fd)
  300.         else:
  301.             # not in PASV mode.
  302.             ip, port = self.client_addr
  303.             cdc = recv_channel (self, self.client_addr, fd)
  304.             cdc.create_socket (socket.AF_INET, socket.SOCK_STREAM)
  305.             try:
  306.                 cdc.connect ((ip, port))
  307.             except socket.error, why:
  308.                 self.respond ("425 Can't build data connection")
  309.         self.client_dc = cdc
  310.  
  311.     type_map = {
  312.         'a':'ASCII',
  313.         'i':'Binary',
  314.         'e':'EBCDIC',
  315.         'l':'Binary'
  316.         }
  317.  
  318.     type_mode_map = {
  319.         'a':'t',
  320.         'i':'b',
  321.         'e':'b',
  322.         'l':'b'
  323.         }
  324.  
  325.     # --------------------------------------------------
  326.     # command methods
  327.     # --------------------------------------------------
  328.  
  329.     def cmd_type (self, line):
  330.         'specify data transfer type'
  331.         # ascii, ebcdic, image, local <byte size>
  332.         t = string.lower (line[1])
  333.         # no support for EBCDIC
  334.         # if t not in ['a','e','i','l']:
  335.         if t not in ['a','i','l']:
  336.             self.command_not_understood (string.join (line))
  337.         elif t == 'l' and (len(line) > 2 and line[2] != '8'):
  338.             self.respond ('504 Byte size must be 8')
  339.         else:
  340.             self.current_mode = t
  341.             self.respond ('200 Type set to %s.' % self.type_map[t])
  342.  
  343.  
  344.     def cmd_quit (self, line):
  345.         'terminate session'
  346.         self.respond ('221 Goodbye.')
  347.         self.close_when_done()
  348.  
  349.     def cmd_port (self, line):
  350.         'specify data connection port'
  351.         info = string.split (line[1], ',')
  352.         ip = string.join (info[:4], '.')
  353.         port = string.atoi(info[4])*256 + string.atoi(info[5])
  354.         # how many data connections at a time?
  355.         # I'm assuming one for now...
  356.         # TODO: we should (optionally) verify that the
  357.         # ip number belongs to the client.  [wu-ftpd does this?]
  358.         self.client_addr = (ip, port)
  359.         self.respond ('200 PORT command successful.')
  360.  
  361.     def new_passive_acceptor (self):
  362.         # ensure that only one of these exists at a time.
  363.         if self.passive_acceptor is not None:
  364.             self.passive_acceptor.close()
  365.             self.passive_acceptor = None
  366.         self.passive_acceptor = passive_acceptor (self)
  367.         return self.passive_acceptor
  368.  
  369.     def cmd_pasv (self, line):
  370.         'prepare for server-to-server transfer'
  371.         pc = self.new_passive_acceptor()
  372.         port = pc.addr[1]
  373.         ip_addr = pc.control_channel.getsockname()[0]
  374.         self.respond (
  375.             '227 Entering Passive Mode (%s,%d,%d)' % (
  376.                 string.join (string.split (ip_addr, '.'), ','),
  377.                 port/256,
  378.                 port%256
  379.                 )
  380.             )
  381.         self.client_dc = None
  382.  
  383.     def cmd_nlst (self, line):
  384.         'give name list of files in directory'
  385.         # ncftp adds the -FC argument for the user-visible 'nlist'
  386.         # command.  We could try to emulate ls flags, but not just yet.
  387.         if '-FC' in line:
  388.             line.remove ('-FC')
  389.         try:
  390.             dir_list_producer = self.get_dir_list (line, 0)
  391.         except os.error, why:
  392.             self.respond ('550 Could not list directory: %s' % repr(why))
  393.             return
  394.         self.respond (
  395.             '150 Opening %s mode data connection for file list' % (
  396.                 self.type_map[self.current_mode]
  397.                 )
  398.             )
  399.         self.make_xmit_channel()
  400.         self.client_dc.push_with_producer (dir_list_producer)
  401.         self.client_dc.close_when_done()
  402.  
  403.     def cmd_list (self, line):
  404.         'give list files in a directory'
  405.         try:
  406.             dir_list_producer = self.get_dir_list (line, 1)
  407.         except os.error, why:
  408.             self.respond ('550 Could not list directory: %s' % repr(why))
  409.             return
  410.         self.respond (
  411.             '150 Opening %s mode data connection for file list' % (
  412.                 self.type_map[self.current_mode]
  413.                 )
  414.             )
  415.         self.make_xmit_channel()
  416.         self.client_dc.push_with_producer (dir_list_producer)
  417.         self.client_dc.close_when_done()
  418.  
  419.     def cmd_cwd (self, line):
  420.         'change working directory'
  421.         if self.cwd (line):
  422.             self.respond ('250 CWD command successful.')
  423.         else:
  424.             self.respond ('550 No such directory.')            
  425.  
  426.     def cmd_cdup (self, line):
  427.         'change to parent of current working directory'
  428.         if self.cdup(line):
  429.             self.respond ('250 CDUP command successful.')
  430.         else:
  431.             self.respond ('550 No such directory.')
  432.         
  433.     def cmd_pwd (self, line):
  434.         'print the current working directory'
  435.         self.respond (
  436.             '257 "%s" is the current directory.' % (
  437.                 self.filesystem.current_directory()
  438.                 )
  439.             )
  440.  
  441.     # modification time
  442.     # example output:
  443.     # 213 19960301204320
  444.     def cmd_mdtm (self, line):
  445.         'show last modification time of file'
  446.         filename = line[1]
  447.         if not self.filesystem.isfile (filename):
  448.             self.respond ('550 "%s" is not a file' % filename)
  449.         else:
  450.             mtime = time.gmtime(self.filesystem.stat(filename)[stat.ST_MTIME])
  451.             self.respond (
  452.                 '213 %4d%02d%02d%02d%02d%02d' % (
  453.                     mtime[0],
  454.                     mtime[1],
  455.                     mtime[2],
  456.                     mtime[3],
  457.                     mtime[4],
  458.                     mtime[5]
  459.                     )
  460.                 )
  461.  
  462.     def cmd_noop (self, line):
  463.         'do nothing'
  464.         self.respond ('200 NOOP command successful.')
  465.  
  466.     def cmd_size (self, line):
  467.         'return size of file'
  468.         filename = line[1]
  469.         if not self.filesystem.isfile (filename):
  470.             self.respond ('550 "%s" is not a file' % filename)
  471.         else:
  472.             self.respond (
  473.                 '213 %d' % (self.filesystem.stat(filename)[stat.ST_SIZE])
  474.                 )
  475.  
  476.     def cmd_retr (self, line):
  477.         'retrieve a file'
  478.         if len(line) < 2:
  479.             self.command_not_understood (string.join (line))
  480.         else:
  481.             file = line[1]
  482.             if not self.filesystem.isfile (file):
  483.                 self.log_info ('checking %s' % file)
  484.                 self.respond ('550 No such file')
  485.             else:
  486.                 try:
  487.                     # FIXME: for some reason, 'rt' isn't working on win95
  488.                     mode = 'r'+self.type_mode_map[self.current_mode]
  489.                     fd = self.open (file, mode)
  490.                 except IOError, why:
  491.                     self.respond ('553 could not open file for reading: %s' % (repr(why)))
  492.                     return
  493.                 self.respond (
  494.                     "150 Opening %s mode data connection for file '%s'" % (
  495.                         self.type_map[self.current_mode],
  496.                         file
  497.                         )
  498.                     )
  499.                 self.make_xmit_channel()
  500.  
  501.                 if self.restart_position:
  502.                     # try to position the file as requested, but
  503.                     # give up silently on failure (the 'file object'
  504.                     # may not support seek())
  505.                     try:
  506.                         fd.seek (self.restart_position)
  507.                     except:
  508.                         pass
  509.                     self.restart_position = 0
  510.  
  511.                 self.client_dc.push_with_producer (
  512.                     file_producer (self, self.client_dc, fd)
  513.                     )
  514.                 self.client_dc.close_when_done()
  515.  
  516.     def cmd_stor (self, line, mode='wb'):
  517.         'store a file'
  518.         if len (line) < 2:
  519.             self.command_not_understood (string.join (line))
  520.         else:
  521.             if self.restart_position:
  522.                 restart_position = 0
  523.                 self.respond ('553 restart on STOR not yet supported')
  524.                 return
  525.             file = line[1]
  526.             # todo: handle that type flag
  527.             try:
  528.                 fd = self.open (file, mode)
  529.             except IOError, why:
  530.                 self.respond ('553 could not open file for writing: %s' % (repr(why)))
  531.                 return
  532.             self.respond (
  533.                 '150 Opening %s connection for %s' % (
  534.                     self.type_map[self.current_mode],
  535.                     file
  536.                     )
  537.                 )
  538.             self.make_recv_channel (fd)
  539.  
  540.     def cmd_abor (self, line):
  541.         'abort operation'
  542.         if self.client_dc:
  543.             self.client_dc.close()
  544.         self.respond ('226 ABOR command successful.')
  545.  
  546.     def cmd_appe (self, line):
  547.         'append to a file'
  548.         return self.cmd_stor (line, 'ab')
  549.  
  550.     def cmd_dele (self, line):
  551.         if len (line) != 2:
  552.             self.command_not_understood (string.join (line))
  553.         else:
  554.             file = line[1]
  555.             if self.filesystem.isfile (file):
  556.                 try:
  557.                     self.filesystem.unlink (file)
  558.                     self.respond ('250 DELE command successful.')
  559.                 except:
  560.                     self.respond ('550 error deleting file.')
  561.             else:
  562.                 self.respond ('550 %s: No such file.' % file)
  563.  
  564.     def cmd_mkd (self, line):
  565.         if len (line) != 2:
  566.             self.command.not_understood (string.join (line))
  567.         else:
  568.             path = line[1]
  569.             try:
  570.                 self.filesystem.mkdir (path)
  571.                 self.respond ('257 MKD command successful.')
  572.             except:
  573.                 self.respond ('550 error creating directory.')
  574.  
  575.     def cmd_rmd (self, line):
  576.         if len (line) != 2:
  577.             self.command.not_understood (string.join (line))
  578.         else:
  579.             path = line[1]
  580.             try:
  581.                 self.filesystem.rmdir (path)
  582.                 self.respond ('250 RMD command successful.')
  583.             except:
  584.                 self.respond ('550 error removing directory.')
  585.  
  586.     def cmd_user (self, line):
  587.         'specify user name'
  588.         if len(line) > 1:
  589.             self.user = line[1]
  590.             self.respond ('331 Password required.')
  591.         else:
  592.             self.command_not_understood (string.join (line))
  593.  
  594.     def cmd_pass (self, line):
  595.         'specify password'
  596.         if len(line) < 2:
  597.             pw = ''
  598.         else:
  599.             pw = line[1]
  600.         result, message, fs = self.server.authorizer.authorize (self, self.user, pw)
  601.         if result:
  602.             self.respond ('230 %s' % message)
  603.             self.filesystem = fs
  604.             self.authorized = 1
  605.             self.log_info('Successful login: Filesystem=%s' % repr(fs))
  606.         else:
  607.             self.respond ('530 %s' % message)
  608.  
  609.     def cmd_rest (self, line):
  610.         'restart incomplete transfer'
  611.         try:
  612.             pos = string.atoi (line[1])
  613.         except ValueError:
  614.             self.command_not_understood (string.join (line))
  615.         self.restart_position = pos
  616.         self.respond (
  617.             '350 Restarting at %d. Send STORE or RETRIEVE to initiate transfer.' % pos
  618.             )
  619.  
  620.     def cmd_stru (self, line):
  621.         'obsolete - set file transfer structure'
  622.         if line[1] in 'fF':
  623.             # f == 'file'
  624.             self.respond ('200 STRU F Ok')
  625.         else:
  626.             self.respond ('504 Unimplemented STRU type')
  627.  
  628.     def cmd_mode (self, line):
  629.         'obsolete - set file transfer mode'
  630.         if line[1] in 'sS':
  631.             # f == 'file'
  632.             self.respond ('200 MODE S Ok')
  633.         else:
  634.             self.respond ('502 Unimplemented MODE type')
  635.  
  636. # The stat command has two personalities.  Normally it returns status
  637. # information about the current connection.  But if given an argument,
  638. # it is equivalent to the LIST command, with the data sent over the
  639. # control connection.  Strange.  But wuftpd, ftpd, and nt's ftp server
  640. # all support it.
  641. #
  642. ##    def cmd_stat (self, line):
  643. ##        'return status of server'
  644. ##        pass
  645.  
  646.     def cmd_syst (self, line):
  647.         'show operating system type of server system'
  648.         # Replying to this command is of questionable utility, because
  649.         # this server does not behave in a predictable way w.r.t. the
  650.         # output of the LIST command.  We emulate Unix ls output, but
  651.         # on win32 the pathname can contain drive information at the front
  652.         # Currently, the combination of ensuring that os.sep == '/'
  653.         # and removing the leading slash when necessary seems to work.
  654.         # [cd'ing to another drive also works]
  655.         #
  656.         # This is how wuftpd responds, and is probably
  657.         # the most expected.  The main purpose of this reply is so that
  658.         # the client knows to expect Unix ls-style LIST output.
  659.         self.respond ('215 UNIX Type: L8')
  660.         # one disadvantage to this is that some client programs
  661.         # assume they can pass args to /bin/ls.
  662.         # a few typical responses:
  663.         # 215 UNIX Type: L8 (wuftpd)
  664.         # 215 Windows_NT version 3.51
  665.         # 215 VMS MultiNet V3.3
  666.         # 500 'SYST': command not understood. (SVR4)
  667.  
  668.     def cmd_help (self, line):
  669.         'give help information'
  670.         # find all the methods that match 'cmd_xxxx',
  671.         # use their docstrings for the help response.
  672.         attrs = dir(self.__class__)
  673.         help_lines = []
  674.         for attr in attrs:
  675.             if attr[:4] == 'cmd_':
  676.                 x = getattr (self, attr)
  677.                 if type(x) == type(self.cmd_help):
  678.                     if x.__doc__:
  679.                         help_lines.append ('\t%s\t%s' % (attr[4:], x.__doc__))
  680.         if help_lines:
  681.             self.push ('214-The following commands are recognized\r\n')
  682.             self.push_with_producer (producers.lines_producer (help_lines))
  683.             self.push ('214\r\n')
  684.         else:
  685.             self.push ('214-\r\n\tHelp Unavailable\r\n214\r\n')
  686.  
  687. class ftp_server (asyncore.dispatcher):
  688.     # override this to spawn a different FTP channel class.
  689.     ftp_channel_class = ftp_channel
  690.  
  691.     SERVER_IDENT = 'FTP Server (V%s)' % VERSION
  692.  
  693.     def __init__ (
  694.         self,
  695.         authorizer,
  696.         hostname    =None,
  697.         ip            ='',
  698.         port        =21,
  699.         resolver    =None,
  700.         logger_object=logger.file_logger (sys.stdout)
  701.         ):
  702.         self.ip = ip
  703.         self.port = port
  704.         self.authorizer = authorizer
  705.  
  706.         if hostname is None:
  707.             self.hostname = socket.gethostname()
  708.         else:
  709.             self.hostname = hostname
  710.  
  711.         # statistics
  712.         self.total_sessions = counter()
  713.         self.closed_sessions = counter()
  714.         self.total_files_out = counter()
  715.         self.total_files_in = counter()
  716.         self.total_bytes_out = counter()
  717.         self.total_bytes_in = counter()
  718.         self.total_exceptions = counter()
  719.         #
  720.         asyncore.dispatcher.__init__ (self)
  721.         self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
  722.  
  723.         self.set_reuse_addr()
  724.         self.bind ((self.ip, self.port))
  725.         self.listen (5)
  726.  
  727.         if not logger_object:
  728.             logger_object = sys.stdout
  729.  
  730.         if resolver:
  731.             self.logger = logger.resolving_logger (resolver, logger_object)
  732.         else:
  733.             self.logger = logger.unresolving_logger (logger_object)
  734.  
  735.         self.log_info('FTP server started at %s\n\tAuthorizer:%s\n\tHostname: %s\n\tPort: %d' % (
  736.             time.ctime(time.time()),
  737.             repr (self.authorizer),
  738.             self.hostname,
  739.             self.port)
  740.             )
  741.  
  742.     def writable (self):
  743.         return 0
  744.  
  745.     def handle_read (self):
  746.         pass
  747.  
  748.     def handle_connect (self):
  749.         pass
  750.  
  751.     def handle_accept (self):
  752.         conn, addr = self.accept()
  753.         self.total_sessions.increment()
  754.         self.log_info('Incoming connection from %s:%d' % (addr[0], addr[1]))
  755.         self.ftp_channel_class (self, conn, addr)
  756.  
  757.     # return a producer describing the state of the server
  758.     def status (self):
  759.  
  760.         def nice_bytes (n):
  761.             return string.join (status_handler.english_bytes (n))
  762.  
  763.         return producers.lines_producer (
  764.             ['<h2>%s</h2>'                % self.SERVER_IDENT,
  765.              '<br>Listening on <b>Host:</b> %s' % self.hostname,
  766.              '<b>Port:</b> %d'            % self.port,
  767.              '<br>Sessions',
  768.              '<b>Total:</b> %s'            % self.total_sessions,
  769.              '<b>Current:</b> %d'        % (self.total_sessions.as_long() - self.closed_sessions.as_long()),
  770.              '<br>Files',
  771.              '<b>Sent:</b> %s'            % self.total_files_out,
  772.              '<b>Received:</b> %s'        % self.total_files_in,
  773.              '<br>Bytes',
  774.              '<b>Sent:</b> %s'            % nice_bytes (self.total_bytes_out.as_long()),
  775.              '<b>Received:</b> %s'        % nice_bytes (self.total_bytes_in.as_long()),
  776.              '<br>Exceptions: %s'        % self.total_exceptions,
  777.              ]
  778.             )
  779.  
  780. # ======================================================================
  781. #                         Data Channel Classes
  782. # ======================================================================
  783.  
  784. # This socket accepts a data connection, used when the server has been
  785. # placed in passive mode.  Although the RFC implies that we ought to
  786. # be able to use the same acceptor over and over again, this presents
  787. # a problem: how do we shut it off, so that we are accepting
  788. # connections only when we expect them?  [we can't]
  789. #
  790. # wuftpd, and probably all the other servers, solve this by allowing
  791. # only one connection to hit this acceptor.  They then close it.  Any
  792. # subsequent data-connection command will then try for the default
  793. # port on the client side [which is of course never there].  So the
  794. # 'always-send-PORT/PASV' behavior seems required.
  795. #
  796. # Another note: wuftpd will also be listening on the channel as soon
  797. # as the PASV command is sent.  It does not wait for a data command
  798. # first.
  799.  
  800. # --- we need to queue up a particular behavior:
  801. #  1) xmit : queue up producer[s]
  802. #  2) recv : the file object
  803. #
  804. # It would be nice if we could make both channels the same.  Hmmm..
  805. #
  806.  
  807. class passive_acceptor (asyncore.dispatcher):
  808.     ready = None
  809.  
  810.     def __init__ (self, control_channel):
  811.         # connect_fun (conn, addr)
  812.         asyncore.dispatcher.__init__ (self)
  813.         self.control_channel = control_channel
  814.         self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
  815.         # bind to an address on the interface that the
  816.         # control connection is coming from.
  817.         self.bind ((
  818.             self.control_channel.getsockname()[0],
  819.             0
  820.             ))
  821.         self.addr = self.getsockname()
  822.         self.listen (1)
  823.  
  824. #    def __del__ (self):
  825. #        print 'passive_acceptor.__del__()'
  826.  
  827.     def log (self, *ignore):
  828.         pass
  829.  
  830.     def handle_accept (self):
  831.         conn, addr = self.accept()
  832.         dc = self.control_channel.client_dc
  833.         if dc is not None:
  834.             dc.set_socket (conn)
  835.             dc.addr = addr
  836.             dc.connected = 1
  837.             self.control_channel.passive_acceptor = None
  838.         else:
  839.             self.ready = conn, addr
  840.         self.close()
  841.  
  842.  
  843. class xmit_channel (asynchat.async_chat):
  844.  
  845.     # for an ethernet, you want this to be fairly large, in fact, it
  846.     # _must_ be large for performance comparable to an ftpd.  [64k] we
  847.     # ought to investigate automatically-sized buffers...
  848.  
  849.     ac_out_buffer_size = 16384
  850.     bytes_out = 0
  851.  
  852.     def __init__ (self, channel, client_addr=None):
  853.         self.channel = channel
  854.         self.client_addr = client_addr
  855.         asynchat.async_chat.__init__ (self)
  856.         
  857. #    def __del__ (self):
  858. #        print 'xmit_channel.__del__()'
  859.  
  860.     def log (*args):
  861.         pass
  862.  
  863.     def readable (self):
  864.         return not self.connected
  865.  
  866.     def writable (self):
  867.         return 1
  868.  
  869.     def send (self, data):
  870.         result = asynchat.async_chat.send (self, data)
  871.         self.bytes_out = self.bytes_out + result
  872.         return result
  873.  
  874.     def handle_error (self):
  875.         # usually this is to catch an unexpected disconnect.
  876.         self.log_info ('unexpected disconnect on data xmit channel', 'error')
  877.         try:
  878.             self.close()
  879.         except:
  880.             pass
  881.  
  882.     # TODO: there's a better way to do this.  we need to be able to
  883.     # put 'events' in the producer fifo.  to do this cleanly we need
  884.     # to reposition the 'producer' fifo as an 'event' fifo.
  885.  
  886.     def close (self):
  887.         c = self.channel
  888.         s = c.server
  889.         c.client_dc = None
  890.         s.total_files_out.increment()
  891.         s.total_bytes_out.increment (self.bytes_out)
  892.         if not len(self.producer_fifo):
  893.             c.respond ('226 Transfer complete')
  894.         elif not c.closed:
  895.             c.respond ('426 Connection closed; transfer aborted')
  896.         del c
  897.         del s
  898.         del self.channel
  899.         asynchat.async_chat.close (self)
  900.  
  901. class recv_channel (asyncore.dispatcher):
  902.     def __init__ (self, channel, client_addr, fd):
  903.         self.channel = channel
  904.         self.client_addr = client_addr
  905.         self.fd = fd
  906.         asyncore.dispatcher.__init__ (self)
  907.         self.bytes_in = counter()
  908.  
  909.     def log (self, *ignore):
  910.         pass
  911.  
  912.     def handle_connect (self):
  913.         pass
  914.  
  915.     def writable (self):
  916.         return 0
  917.  
  918.     def recv (*args):
  919.         result = apply (asyncore.dispatcher.recv, args)
  920.         self = args[0]
  921.         self.bytes_in.increment(len(result))
  922.         return result
  923.  
  924.     buffer_size = 8192
  925.  
  926.     def handle_read (self):
  927.         block = self.recv (self.buffer_size)
  928.         if block:
  929.             try:
  930.                 self.fd.write (block)
  931.             except IOError:
  932.                 self.log_info ('got exception writing block...', 'error')
  933.  
  934.     def handle_close (self):
  935.         s = self.channel.server
  936.         s.total_files_in.increment()
  937.         s.total_bytes_in.increment(self.bytes_in.as_long())
  938.         self.fd.close()
  939.         self.channel.respond ('226 Transfer complete.')
  940.         self.close()
  941.  
  942. import filesys
  943.  
  944. # not much of a doorman! 8^)
  945. class dummy_authorizer:
  946.     def __init__ (self, root='/'):
  947.         self.root = root
  948.     def authorize (self, channel, username, password):
  949.         channel.persona = -1, -1
  950.         channel.read_only = 1
  951.         return 1, 'Ok.', filesys.os_filesystem (self.root)
  952.  
  953. class anon_authorizer:
  954.     def __init__ (self, root='/'):
  955.         self.root = root
  956.         
  957.     def authorize (self, channel, username, password):
  958.         if username in ('ftp', 'anonymous'):
  959.             channel.persona = -1, -1
  960.             channel.read_only = 1
  961.             return 1, 'Ok.', filesys.os_filesystem (self.root)
  962.         else:
  963.             return 0, 'Password invalid.', None
  964.  
  965. # ===========================================================================
  966. # Unix-specific improvements
  967. # ===========================================================================
  968.  
  969. if os.name == 'posix':
  970.  
  971.     class unix_authorizer:
  972.         # return a trio of (success, reply_string, filesystem)
  973.         def authorize (self, channel, username, password):
  974.             import crypt
  975.             import pwd
  976.             try:
  977.                 info = pwd.getpwnam (username)
  978.             except KeyError:
  979.                 return 0, 'No such user.', None
  980.             mangled = info[1]
  981.             if crypt.crypt (password, mangled[:2]) == mangled:
  982.                 channel.read_only = 0
  983.                 fs = filesys.schizophrenic_unix_filesystem (
  984.                     '/',
  985.                     info[5],
  986.                     persona = (info[2], info[3])
  987.                     )
  988.                 return 1, 'Login successful.', fs
  989.             else:
  990.                 return 0, 'Password invalid.', None
  991.  
  992.         def __repr__ (self):
  993.             return '<standard unix authorizer>'
  994.  
  995.     # simple anonymous ftp support
  996.     class unix_authorizer_with_anonymous (unix_authorizer):
  997.         def __init__ (self, root=None, real_users=0):
  998.             self.root = root
  999.             self.real_users = real_users
  1000.  
  1001.         def authorize (self, channel, username, password):
  1002.             if string.lower(username) in ['anonymous', 'ftp']:
  1003.                 import pwd
  1004.                 try:
  1005.                     # ok, here we run into lots of confusion.
  1006.                     # on some os', anon runs under user 'nobody',
  1007.                     # on others as 'ftp'.  ownership is also critical.
  1008.                     # need to investigate.
  1009.                     # linux: new linuxen seem to have nobody's UID=-1,
  1010.                     #    which is an illegal value.  Use ftp.
  1011.                     ftp_user_info = pwd.getpwnam ('ftp')
  1012.                     if string.lower(os.uname()[0]) == 'linux':
  1013.                         nobody_user_info = pwd.getpwnam ('ftp')
  1014.                     else:
  1015.                         nobody_user_info = pwd.getpwnam ('nobody')
  1016.                     channel.read_only = 1
  1017.                     if self.root is None:
  1018.                         self.root = ftp_user_info[5]
  1019.                     fs = filesys.unix_filesystem (self.root, '/')
  1020.                     return 1, 'Anonymous Login Successful', fs
  1021.                 except KeyError:
  1022.                     return 0, 'Anonymous account not set up', None
  1023.             elif self.real_users:
  1024.                 return unix_authorizer.authorize (
  1025.                     self,
  1026.                     channel,
  1027.                     username,
  1028.                     password
  1029.                     )
  1030.             else:
  1031.                 return 0, 'User logins not allowed', None
  1032.  
  1033. class file_producer:
  1034.     block_size = 16384
  1035.     def __init__ (self, server, dc, fd):
  1036.         self.fd = fd
  1037.         self.done = 0
  1038.         
  1039.     def more (self):
  1040.         if self.done:
  1041.             return ''
  1042.         else:
  1043.             block = self.fd.read (self.block_size)
  1044.             if not block:
  1045.                 self.fd.close()
  1046.                 self.done = 1
  1047.             return block
  1048.  
  1049. # usage: ftp_server /PATH/TO/FTP/ROOT PORT
  1050. # for example:
  1051. # $ ftp_server /home/users/ftp 8021
  1052.  
  1053. if os.name == 'posix':
  1054.     def test (port='8021'):
  1055.         import sys
  1056.         fs = ftp_server (
  1057.             unix_authorizer(),
  1058.             port=string.atoi (port)
  1059.             )
  1060.         try:
  1061.             asyncore.loop()
  1062.         except KeyboardInterrupt:
  1063.             self.log_info('FTP server shutting down. (received SIGINT)', 'warning')
  1064.             # close everything down on SIGINT.
  1065.             # of course this should be a cleaner shutdown.
  1066.             asyncore.close_all()
  1067.  
  1068.     if __name__ == '__main__':
  1069.         test (sys.argv[1])
  1070. # not unix
  1071. else:
  1072.     def test ():
  1073.         fs = ftp_server (dummy_authorizer())
  1074.     if __name__ == '__main__':
  1075.         test ()
  1076.  
  1077. # this is the command list from the wuftpd man page
  1078. # '*' means we've implemented it.
  1079. # '!' requires write access
  1080. #
  1081. command_documentation = {
  1082.     'abor':    'abort previous command',                            #*
  1083.     'acct':    'specify account (ignored)',
  1084.     'allo':    'allocate storage (vacuously)',
  1085.     'appe':    'append to a file',                                    #*!
  1086.     'cdup':    'change to parent of current working directory',    #*
  1087.     'cwd':    'change working directory',                            #*
  1088.     'dele':    'delete a file',                                    #!
  1089.     'help':    'give help information',                            #*
  1090.     'list':    'give list files in a directory',                    #*
  1091.     'mkd':    'make a directory',                                    #!
  1092.     'mdtm':    'show last modification time of file',                #*
  1093.     'mode':    'specify data transfer mode',
  1094.     'nlst':    'give name list of files in directory',                #*
  1095.     'noop':    'do nothing',                                        #*
  1096.     'pass':    'specify password',                                    #*
  1097.     'pasv':    'prepare for server-to-server transfer',            #*
  1098.     'port':    'specify data connection port',                        #*
  1099.     'pwd':    'print the current working directory',                #*
  1100.     'quit':    'terminate session',                                #*
  1101.     'rest':    'restart incomplete transfer',                        #*
  1102.     'retr':    'retrieve a file',                                    #*
  1103.     'rmd':    'remove a directory',                                #!
  1104.     'rnfr':    'specify rename-from file name',                    #!
  1105.     'rnto':    'specify rename-to file name',                        #!
  1106.     'site':    'non-standard commands (see next section)',
  1107.     'size':    'return size of file',                                #*
  1108.     'stat':    'return status of server',                            #*
  1109.     'stor':    'store a file',                                        #*!
  1110.     'stou':    'store a file with a unique name',                    #!
  1111.     'stru':    'specify data transfer structure',
  1112.     'syst':    'show operating system type of server system',        #*
  1113.     'type':    'specify data transfer type',                        #*
  1114.     'user':    'specify user name',                                #*
  1115.     'xcup':    'change to parent of current working directory (deprecated)',
  1116.     'xcwd':    'change working directory (deprecated)',
  1117.     'xmkd':    'make a directory (deprecated)',                    #!
  1118.     'xpwd':    'print the current working directory (deprecated)',
  1119.     'xrmd':    'remove a directory (deprecated)',                    #!
  1120. }
  1121.  
  1122.  
  1123. # debugging aid (linux)
  1124. def get_vm_size ():
  1125.     return string.atoi (string.split(open ('/proc/self/stat').readline())[22])
  1126.  
  1127. def print_vm():
  1128.     print 'vm: %8dk' % (get_vm_size()/1024)
  1129.